<script setup lang="tsx">
import type { RuleObject } from 'ant-design-vue/es/form/'
import type { ComponentType } from './componentMap'
import type { ComponentProps, CustomRenderFn, FormSchema, RenderCallbackParams } from './types/'
import { Col, Divider, Form, Spin } from 'ant-design-vue'
import { cloneDeep, debounce, isEqual } from 'lodash-es'
import { computed, isVNode, nextTick, toRefs, unref, watch } from 'vue'
import BasicHelp from '@/components/basic/basic-help/index.vue'
import { useI18n } from '@/hooks/useI18n'
import { isArray, isBoolean, isFunction, isNull, isObject, isString } from '@/utils/is'
import { componentMap } from './componentMap'
import { createPlaceholderMessage } from './helper'
import { useFormContext } from './hooks/useFormContext'
import { useItemLabelWidth } from './hooks/useLabelWidth'
import { schemaFormItemProps } from './schema-form-item'

defineOptions({
  name: 'SchemaFormItem',
})

const props = defineProps(schemaFormItemProps)
const emit = defineEmits(['update:formModel'])

// schemaForm组件实例
const formContext = useFormContext()
const { formPropsRef, setItemRef, updateSchema, getSchemaByField, appendSchemaByField }
    = formContext

const { t } = useI18n()

const { schema } = toRefs(props)

const itemLabelWidthProp = useItemLabelWidth(schema, formPropsRef)

const namePath = computed<string[]>(() => {
  return isArray(schema.value.field) ? schema.value.field : schema.value.field.split('.')
})

const modelValue = computed({
  get() {
    return namePath.value.reduce((prev, field) => prev?.[field], props.formModel)
  },
  set(val) {
    const namePath = schema.value.field.split('.')
    const prop = namePath.pop()!
    const target = namePath.reduce((prev, field) => (prev[field] ??= {}), props.formModel)
    target[prop] = val
    emit('update:formModel', props.formModel)
  },
})

const modelValueType = computed<string>(() => {
  const { component, componentProps } = schema.value
  if (!isFunction(componentProps) && componentProps?.vModelKey) {
    return componentProps.vModelKey
  }
  const isCheck = isString(component) && ['Switch', 'Checkbox'].includes(component)
  const isUpload = component === 'Upload'
  return {
    true: 'value',
    [`${isCheck}`]: 'checked',
    [`${isUpload}`]: 'file-list',
  }.true
})

// @ts-ignore
const getValues = computed<RenderCallbackParams>(() => {
  const { formModel, schema, tableInstance } = props

  const { mergeDynamicData } = unref(formPropsRef)
  return {
    field: schema.field,
    formInstance: formContext,
    tableInstance,
    tableRowKey: props.tableRowKey,
    formModel: props.tableRowKey ? formModel[props.tableRowKey] : formModel,
    values: {
      ...mergeDynamicData,
      ...formModel,
    } as Recordable,
    schema: computed(() => props.schema),
  }
})

const getShow = computed<{ isShow: boolean, isIfShow: boolean }>(() => {
  const { vShow, vIf, isAdvanced = false } = unref(schema)
  const { showAdvancedButton } = unref(formPropsRef)
  const itemIsAdvanced = showAdvancedButton ? (isBoolean(isAdvanced) ? isAdvanced : true) : true

  let isShow = true
  let isIfShow = true

  if (isBoolean(vShow)) {
    isShow = vShow
  }
  if (isBoolean(vIf)) {
    isIfShow = vIf
  }
  if (isFunction(vShow)) {
    isShow = vShow(unref(getValues))
  }
  if (isFunction(vIf)) {
    isIfShow = vIf(unref(getValues))
  }
  isShow = isShow && itemIsAdvanced

  return { isShow, isIfShow }
})

const getDisable = computed(() => {
  const { disabled: globDisabled } = unref(formPropsRef)
  const { dynamicDisabled } = props.schema
  const { disabled: itemDisabled = false } = unref(getComponentProps)
  let disabled = !!globDisabled || itemDisabled
  if (isBoolean(dynamicDisabled)) {
    disabled = dynamicDisabled
  }
  if (isFunction(dynamicDisabled)) {
    disabled = dynamicDisabled(unref(getValues))
  }
  return disabled
})

const vnodeFactory = (
  component: FormSchema['componentSlots'] | FormSchema['component'],
  values = unref(getValues),
) => {
  if (isString(component)) {
    return <>{component}</>
  }
  else if (isVNode(component)) {
    return component
  }
  else if (isFunction(component)) {
    return vnodeFactory((component as CustomRenderFn)(values))
  }
  else if (component && isObject(component)) {
    const compKeys = Object.keys(component)
    // 如果是组件对象直接return
    if (compKeys.some(n => n.startsWith('_') || ['setup', 'render'].includes(n))) {
      return component
    }
    return compKeys.reduce<Recordable<CustomRenderFn>>((slots, slotName) => {
      slots[slotName] = (...rest: any) => vnodeFactory(component[slotName], ...rest)
      return slots
    }, {})
  }
  return component
}

/**
 * @description 当前表单项组件
 */
const getComponent = computed(() => {
  const component = props.schema.component
  return isString(component)
    ? (componentMap[component] ?? vnodeFactory(component))
    : vnodeFactory(component)
})

/**
 * @description 当前表单项组件的插槽
 */
const getComponentSlots = computed<Recordable<CustomRenderFn>>(() => {
  const componentSlots = props.schema.componentSlots ?? {}
  return isString(componentSlots) || isVNode(componentSlots)
    ? {
        default: (...rest: any) => vnodeFactory(componentSlots, rest),
      }
    : vnodeFactory(componentSlots)
})

const getLabel = computed(() => {
  const label = props.schema.label
  return isFunction(label) ? label(unref(getValues)) : label
})

/**
 * @description 表单组件props
 */
const getComponentProps = computed(() => {
  const { schema } = props
  let { componentProps = {}, component } = schema

  if (isFunction(componentProps)) {
    componentProps = componentProps(unref(getValues)) ?? {}
  }

  if (component !== 'RangePicker' && isString(component)) {
    componentProps.placeholder ??= createPlaceholderMessage(component, getLabel.value)
  }
  if (schema.component === 'Divider') {
    componentProps = Object.assign({ type: 'horizontal' }, componentProps, {
      orientation: 'left',
      plain: true,
    })
  }
  if (isVNode(getComponent.value)) {
    Object.assign(componentProps, getComponent.value.props)
  }

  return componentProps
})

/**
 * @description 表单组件事件
 */
const componentEvents = computed(() => {
  const componentProps = getComponentProps.value
  return Object.keys(componentProps).reduce((prev, key) => {
    if (/^on([A-Z])/.test(key)) {
      // e.g: onChange => change
      const eventKey = key.replace(/^on([A-Z])/, '$1').toLocaleLowerCase()
      prev[eventKey] = componentProps[key]
    }
    return prev
  }, {})
})

const renderLabelHelpMessage = computed(() => {
  const { helpMessage, helpComponentProps, subLabel } = props.schema
  const renderLabel = subLabel
    ? (
        <span>
          {getLabel.value}
          {' '}
          <span class="text-secondary">{subLabel}</span>
        </span>
      )
    : (
        vnodeFactory(getLabel.value)
      )
  const getHelpMessage = isFunction(helpMessage) ? helpMessage(unref(getValues)) : helpMessage
  if (!getHelpMessage || (Array.isArray(getHelpMessage) && getHelpMessage.length === 0)) {
    return renderLabel
  }
  return (
    <span>
      {renderLabel}
      <BasicHelp placement="top" class="mx-1" text={getHelpMessage} {...helpComponentProps} />
    </span>
  )
})

function setComponentRuleType(rule: RuleObject, component: ComponentType, valueFormat: string) {
  if (['DatePicker', 'MonthPicker', 'WeekPicker', 'TimePicker'].includes(component)) {
    rule.type = valueFormat ? 'string' : 'object'
  }
  else if (['RangePicker', 'Upload', 'CheckboxGroup', 'TimePicker'].includes(component)) {
    rule.type = 'array'
  }
  else if (['InputNumber'].includes(component)) {
    rule.type = 'number'
  }
}

const getRules = computed(() => {
  const {
    rules: defRules = [],
    component,
    rulesMessageJoinLabel,
    dynamicRules,
    required,
  } = props.schema

  if (isFunction(dynamicRules)) {
    return dynamicRules(unref(getValues)) as RuleObject[]
  }

  let rules = cloneDeep<RuleObject[]>(defRules)
  const { rulesMessageJoinLabel: globalRulesMessageJoinLabel } = unref(formPropsRef)

  const joinLabel = Reflect.has(unref(formPropsRef), 'rulesMessageJoinLabel')
    ? rulesMessageJoinLabel
    : globalRulesMessageJoinLabel
  const defaultMsg = isString(component)
    ? `${createPlaceholderMessage(component, getLabel.value)}${joinLabel ? getLabel.value : ''}`
    : undefined

  function validator(rule: any, value: any) {
    const msg = rule.message || defaultMsg

    if (value === undefined || isNull(value)) {
      // 空值
      return Promise.reject(msg)
    }
    else if (Array.isArray(value) && value.length === 0) {
      // 数组类型
      return Promise.reject(msg)
    }
    else if (typeof value === 'string' && value.trim() === '') {
      // 空字符串
      return Promise.reject(msg)
    }
    else if (
      typeof value === 'object'
      && Reflect.has(value, 'checked')
      && Reflect.has(value, 'halfChecked')
      && Array.isArray(value.checked)
      && Array.isArray(value.halfChecked)
      && value.checked.length === 0
      && value.halfChecked.length === 0
    ) {
      // 非关联选择的tree组件
      return Promise.reject(msg)
    }
    return Promise.resolve()
  }

  const getRequired = isFunction(required) ? required(unref(getValues)) : required

  if ((!rules || rules.length === 0) && getRequired) {
    rules = [{ required: getRequired, validator }]
  }

  const requiredRuleIndex: number = rules.findIndex(
    rule => Reflect.has(rule, 'required') && !Reflect.has(rule, 'validator'),
  )

  if (requiredRuleIndex !== -1) {
    const rule = rules[requiredRuleIndex]

    if (component && isString(component)) {
      if (!Reflect.has(rule, 'type')) {
        rule.type = component === 'InputNumber' ? 'number' : 'string'
      }

      rule.message = rule.message || defaultMsg

      if (component.includes('Input') || component.includes('Textarea')) {
        rule.whitespace = true
      }
      const valueFormat = unref(getComponentProps)?.valueFormat
      setComponentRuleType(rule, component, valueFormat)
    }
  }

  // Maximum input length rule check
  const characterInx = rules.findIndex(val => val.max)
  if (characterInx !== -1 && !rules[characterInx].validator) {
    rules[characterInx].message
        = rules[characterInx].message
          || t('component.form.maxTip', [rules[characterInx].max] as Recordable)
  }

  return rules
})

const fetchRemoteData = async (request: PromiseFn<RenderCallbackParams, any>) => {
  try {
    const newSchema = Object.assign(schema.value, {
      loading: true,
      componentProps: {
        ...unref(getComponentProps),
        options: [],
      },
    })

    updateSchema(newSchema)

    const result = await request(unref(getValues))
    const { component } = unref(schema)
    const componentProps = newSchema.componentProps as ComponentProps

    if (['Select', 'RadioGroup', 'CheckboxGroup'].includes(component)) {
      componentProps.options = result
    }
    else if (['TreeSelect', 'Tree'].includes(component)) {
      componentProps.treeData = result
    }
    if (newSchema.componentProps) {
      newSchema.componentProps.requestResult = result
    }
    newSchema.loading = false
    updateSchema(newSchema)
  }
  finally {
    await nextTick()
    schema.value.loading = false
  }
}

const initRequestConfig = () => {
  const request = getComponentProps.value.request
  if (request) {
    if (isFunction(request)) {
      fetchRemoteData(request)
    }
    else {
      const { watchFields = [], options = {}, wait = 0, callback } = request
      const params = watchFields.map(field => () => props.formModel[field])
      watch(
        params,
        debounce(() => {
          fetchRemoteData(callback)
        }, wait),
        {
          ...options,
        },
      )
    }
  }
}

watch(
  getShow,
  (val, oldVal) => {
    if (!isEqual(val, oldVal) && val.isIfShow && val.isShow) {
      if (!getSchemaByField(props.schema.field)) {
        appendSchemaByField(props.schema)
      }
      initRequestConfig()
    }
  },
  {
    immediate: true,
  },
)
</script>

<template>
  <Col v-if="getShow.isIfShow" v-show="getShow.isShow" v-bind="schema.colProps">
    <Divider v-if="schema.component === 'Divider'" v-bind="Object.assign(getComponentProps)">
      <component :is="renderLabelHelpMessage" />
    </Divider>
    <Form.Item
      v-else
      v-bind="{ ...schema.formItemProps, ...itemLabelWidthProp }"
      :label="renderLabelHelpMessage"
      :name="namePath"
      :rules="getRules"
    >
      <!-- 前置插槽 -->
      <template v-if="schema.beforeSlot">
        <slot v-if="isString(schema.beforeSlot)" :name="schema.beforeSlot" v-bind="getValues">
          <span class="mr-[6px]">{{ schema.beforeSlot }}</span>
        </slot>
        <component :is="schema.beforeSlot(getValues)" v-if="isFunction(schema.beforeSlot)" />
      </template>
      <!-- 自定义插槽 -->
      <slot v-if="schema.slot" :name="schema.slot" v-bind="getValues" />
      <template v-else-if="getComponent">
        <component
          :is="getComponent"
          :ref="setItemRef(schema.field)"
          v-bind="getComponentProps"
          v-model:[modelValueType]="modelValue"
          :allow-clear="true"
          :disabled="getDisable"
          :loading="schema.loading"
          v-on="componentEvents"
        >
          <template v-if="Object.is(schema.loading, true)" #notFoundContent>
            <Spin size="small" />
          </template>
          <template
            v-for="(slotFn, slotName) in getComponentSlots"
            #[slotName]="slotData"
            :key="slotName"
          >
            <component :is="slotFn?.({ ...getValues, slotData }) ?? slotFn" :key="slotName" />
          </template>
        </component>
      </template>
      <!-- 后置插槽 -->
      <template v-if="schema.afterSlot">
        <slot v-if="isString(schema.afterSlot)" :name="schema.afterSlot" v-bind="getValues">
          <span class="ml-[6px]">{{ schema.afterSlot }}</span>
        </slot>
        <component :is="schema.afterSlot(getValues)" v-if="isFunction(schema.afterSlot)" />
      </template>
    </Form.Item>
  </Col>
</template>

<style lang="less" scoped>
  :deep(.ant-form-item-control-input-content) {
  display: flex;
  align-items: center;

  > div {
    flex: auto;
  }
}
</style>
